/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.archivers.tar.fileEntries.headers;

import com.softwarecraftsmen.archivers.tar.FileNameIsTooLongException;
import com.softwarecraftsmen.archivers.tar.NameIsTooLongException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import java.io.StringWriter;
import static java.lang.String.format;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import static java.util.Locale.UK;

public class ByteWriter
{
	private static final Charset UsAscii = Charset.forName("US-ASCII");
	@NonNls
	private static final String Zero = "0";
	private static final int NumberOfBitsToRepresentAnOctalNumber = 3;
	private static final int NumberOfBitsInASignedLong = 64;
	private static final int NumberOfBitsInAnUnsignedLong = NumberOfBitsInASignedLong - 1;
	private static final int HighestBitInAnUnsignedLong = NumberOfBitsInAnUnsignedLong - 1;
	private static final byte NUL = (byte) 0;
	private static final byte SP = (byte) 32;
	private static final int FileNameLength = 100;
	private static final int NamePrefixLength = 155;
	private static final int MaximumFileNameLength = FileNameLength + NamePrefixLength;
	private static final int NameOffset = 0;
	private static final int PrefixOffset = 345;

	private final byte[] headerBlock;

	public ByteWriter(final @NotNull byte[] headerBlock)
	{
		this.headerBlock = headerBlock;
	}
//TODO: Flawed; should use ASCII byte form of file name...
	public void writeFileName(final @NotNull String fileName)
	{
		if (fileName.length() <= FileNameLength)
		{
			writeNameField(fileName, NameOffset, FileNameLength);
			return;
		}
		int index = fileName.indexOf("/", fileName.length() - FileNameLength - 1);
		if (index == -1)
		{
			throw new FileNameIsTooLongException(format(UK, "file name '%1$s' can not be broken on a '/' at less than %2$s characters", fileName, FileNameLength), MaximumFileNameLength);
		}

		final String prefix = fileName.substring(0, index);
		if (prefix.length() > NamePrefixLength)
		{
			throw new FileNameIsTooLongException(format(UK, "fileName prefix %1$s is greater than %2$s characters", prefix, NamePrefixLength), MaximumFileNameLength);
		}
		writeNameField(prefix, PrefixOffset, NamePrefixLength);

		final String suffix = fileName.substring(index + 1);
		writeNameField(suffix, NameOffset, FileNameLength);
	}

	public void writeNameField(final @NotNull String name, final int offset, final int fieldLength)
	{
		final int maximumNameLength = fieldLength - 1;

		final ByteBuffer byteBuffer = UsAscii.encode(name);
		final int numberOfBytesWritten = byteBuffer.limit();
		if (numberOfBytesWritten > maximumNameLength)
		{
			throw new NameIsTooLongException(format(UK, "The encoded name %1$s is longer than the permitted maximum size (%2$s) for the header field", name, maximumNameLength), maximumNameLength);
		}
		byteBuffer.get(headerBlock, offset, numberOfBytesWritten);
	}

	public void writeAsNullTerminatedOctalAsciiString(final long unsigned63BitValue, final int offset, final int fieldLength)
	{
		final String asOctalString = asOctalString(unsigned63BitValue);
		final String paddedOctalString = padNumberStringWithLeadingZeros(asOctalString, fieldLength - 1);
		final ByteBuffer byteBuffer = UsAscii.encode(paddedOctalString);
		final int numberOfBytesWritten = byteBuffer.limit();
		assert numberOfBytesWritten == (fieldLength - 1);
		byteBuffer.get(headerBlock, offset, numberOfBytesWritten);
		headerBlock[offset + fieldLength - 1] = NUL;
	}

	public void pad(final int offset)
	{
		/*
		for (int index = offset; index < headerBlock.length;)
		{
			headerBlock[index++] = NUL;
		}
		*/
	}

	public void writeCheckSumOctalBytes(final long value, final int offset, final int fieldLength)
	{
		writeAsNullTerminatedOctalAsciiString(value, offset, fieldLength - 1);
		headerBlock[offset + fieldLength - 1] = SP;
	}

	public void writeLinkFlag(final @NotNull LinkFlag linkFlag)
	{
		linkFlag.write(headerBlock);
	}

	private String asOctalString(final long unsigned63BitValue)
	{
		final StringWriter writer = new StringWriter();
		for(int bitsOfInterest = HighestBitInAnUnsignedLong; bitsOfInterest >= 0; bitsOfInterest -= NumberOfBitsToRepresentAnOctalNumber)
		{
			final long digit = (unsigned63BitValue >>> bitsOfInterest) & 7;
			if (digit == 0 && bitsOfInterest > 0)
			{
				continue;
			}
			writer.write(String.valueOf(digit));
		}
		return writer.toString();
	}

	@NotNull
	private String padNumberStringWithLeadingZeros(final @NotNull String numberString, final int requiredLength)
	{
		if (numberString.length() > requiredLength)
		{
			throw new IllegalArgumentException(format(UK, "numberString %1$s is greater than the required length %2$s", numberString, requiredLength));
		}
		final StringWriter writer = new StringWriter();
		for (int counter = 0; counter < requiredLength - numberString.length(); counter++)
		{
			writer.write(Zero);
		}
		writer.write(numberString);
		return writer.toString();
	}

	public int byteAt(final int index)
	{
		return headerBlock[index];
	}
}
